/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024.
 * Description: /sys/kernel/security/ko_digestlist interface support
 * Author: xiongshenglan
 * Create: 2024-03-29
 */

#include <linux/kernel_read_file.h>
#include <linux/init.h>
#include <linux/seq_file.h>
#include <linux/rculist.h>
#include <linux/rcupdate.h>
#include <linux/vmalloc.h>
#include <crypto/sha2.h>
#include <crypto/hash.h>
#include <crypto/hash_info.h>
#include <linux/hashtable.h>
#include <linux/module.h>
#include <linux/security.h>
#include <linux/module_signature.h>

#include <linux/crl_revocate.h>
#include <internal/rtos_module_digest.h>
#include <internal/rtos_sec_internal.h>

static struct dentry *ko_digest_list_data;
static DEFINE_MUTEX(digest_write_mutex);
static struct ko_digest_bulk digest_list;

/*
 * Verify the signature on a file, borrow from mod_verify_sig
 */
static int file_verify_sig(const char *data, size_t *len, const char *name)
{
	struct module_signature ms;
	size_t sig_len;
	int r;

	if (*len <= sizeof(ms))
		return -EBADMSG;

	memcpy(&ms, data + (*len - sizeof(ms)), sizeof(ms));

	r = mod_check_sig(&ms, *len, name);
	if (r < 0)
		return r;

	sig_len = be32_to_cpu(ms.sig_len);

	/* update len to discard the signature */
	*len -= sig_len + sizeof(ms);

	/*
	 * Check in advance and skip if the signature
	 * is not enabled to prevent the signature
	 * verification process from failing to print an error log
	 */

	return kernel_verify_cms_data(data, *len, data + *len, sig_len);
}

/*
 * If the data has a signature, *len returns the length of the size without the signature.
 */
static int file_sig_check(const char *data, size_t *len, const char *name)
{
	int err = -ENODATA;
	const size_t markerlen = sizeof(MODULE_SIG_STRING) - 1;

	if (*len > markerlen && memcmp(data + *len - markerlen,
		MODULE_SIG_STRING, markerlen) == 0) {
		/* We truncate data to discard the signature */
		*len -= markerlen;
		err = file_verify_sig(data, len, name);
	}

	if (err < 0) {
		pr_err("Loading file: %s is rejected, r:%d", name, err);
		return -EKEYREJECTED;
	}

	return 0;
}


static int ko_digest_parser(const char *field, struct ko_digest_item *item)
{
	u8 *digest;

	/* 1. check digest len */
	if (strlen(field) != SHA256_BLOCK_SIZE) {
		pr_err("Unexpected digest: %s, we expected len: %zu",
				field, SHA256_BLOCK_SIZE);
		return -EINVAL;
	}

	/* 2. try to convert from hex to binary */
	digest = kzalloc(SHA256_DIGEST_SIZE, GFP_KERNEL);
	if (!digest)
		return -ENOMEM;

	if (hex2bin(digest, field, SHA256_DIGEST_SIZE) != 0) {
		kfree(digest);
		pr_err("Cannot convert hex:%s to sha256 binary", field);
		return -EINVAL;
	}

	item->digest = digest;
	return 0;
}

static unsigned int ko_hash_key(const u8 *digest, size_t digest_size)
{
	if (digest_size != SHA256_DIGEST_SIZE) {
		WARN_ONCE(1, "%s got a unexpected digest_size: %zu", __func__, digest_size);
		/* 0 is a valid key for all possible cases and we warn it */
		return 0;
	}

	return (digest[0] | (digest[1] << BITS_PER_BYTE)) % KO_HASH_BITS;
}

static int ko_digest_add_lock(struct ko_digest_item *item)
{
	if (!item)
		return -EINVAL;

	spin_lock(&digest_list.lock);
	if (digest_list.max != SIZE_MAX && digest_list.count >= digest_list.max) {
		spin_unlock(&digest_list.lock);
		return -EFBIG;
	}

	hash_add_rcu(digest_list.hash_head, &item->hash_node,
		ko_hash_key(item->digest, digest_list.digest_size));
	list_add_tail_rcu(&item->list_node, &digest_list.list_head);
	/*
	 * wraparound, overflow is allowed here, no need to detect
	 * because this code path only happened when digest_list.count == SIZE_MAX,
	 * which also means no limit.
	 */
	digest_list.count++;
	spin_unlock(&digest_list.lock);
	return 0;
}

static ssize_t ko_digest_parse_add(char *line, size_t buf_size)
{
	size_t len;
	ssize_t r;
	struct ko_digest_item *item;

	len = strnlen(line, buf_size) + 1;
	if (len > buf_size) {
		pr_err("the calculated len of string is larger than buf_size");
		return -EINVAL;
	}

	/* skip comment line */
	if (*line == '#' || *line == '\0')
		return len;

	item = kzalloc(sizeof(*item), GFP_KERNEL);
	if (!item)
		return -ENOMEM;

	pr_debug("ko_digest_line_parser: %s", line);
	r = ko_digest_parser(line, item);
	if (r < 0) {
		kfree(item);
		return r;
	}
	r = ko_digest_add_lock(item);
	if (r < 0) {
		kfree(item->digest);
		item->digest = NULL;
		kfree(item);
		return r;
	}

	return (ssize_t)len;
}

static int ko_parser_read_from_path(char *path)
{
	void *data = NULL;
	char *datap = NULL;
	char *pathp = NULL;
	char *p = NULL;
	ssize_t size = 0;
	ssize_t r;

	/*
	 * Remove "\n", users will usally append "\n" to the end.
	 * If \n found, the first \n will be overwritten by NULL
	 * NOTE: If \n not found, datap is asigned to NULL
	 * Please read man strsep if u modify this function.
	 */
	pathp = path;
	strsep(&pathp, "\n");

	/* vmalloc is called in kernel_read_file_from_path */
	r = kernel_read_file_from_path(path, 0, &data, MAX_FILE_INPUT_LEN, NULL,
								   READING_POLICY);
	if (r < 0) {
		pr_err("Unable to read file, r: %zd", r);
		return r;
	}
	size = r;

	r = file_sig_check(data, &size, path);
	if (r < 0) {
		pr_err("Loading %s signature check failed\n", path);
		vfree(data);
		return r;
	}

	/*
	 * Read and parse line by line and insert it.
	 * If any error happends, then stop reading.
	 * But the valid line won't be clear.
	 */
	datap = data;
	while (size > 0 && (p = strsep(&datap, "\n"))) {
		r = ko_digest_parse_add(p, (size_t)size);
		if (r < 0) {
			pr_err("ko_digest_parse_add fail, r: %zd", r);
			break;
		} else if (r > size) {
			pr_err("ko_digest_parse_add fail");
			break;
		}
		size -= r;
	}
	vfree(data);
	if (r < 0)
		return r;
	/* the content of data don't run out */
	if (size != 0)
		return -EINVAL;
	return 0;
}

static ssize_t ko_write_digest_list(struct file *file, const char __user *buf,
				size_t datalen, loff_t *ppos)
{
	char *data;
	ssize_t result;

	/* No partial writes. */
	result = -EINVAL;
	if (*ppos != 0)
		return result;

	data = memdup_user_nul(buf, datalen);
	if (IS_ERR(data)) {
		result = PTR_ERR(data);
		return result;
	}

	if (mutex_lock_interruptible(&digest_write_mutex) < 0) {
		result = -EINTR;
		kfree(data);
		return result;
	}

	result = ko_parser_read_from_path(data);
	if (result < 0)
		pr_err("ko_parser_read_from_path failed, result: %d\n", (int)result);

	kfree(data);
	mutex_unlock(&digest_write_mutex);
	return result < 0 ? result : datalen;
}

static void *ko_digest_list_next(struct seq_file *m, void *v, loff_t *pos)
{
	struct ko_digest_item *item = v;

	item = list_next_or_null_rcu(&digest_list.list_head, &item->list_node,
								 struct ko_digest_item, list_node);
	(*pos)++;
	return item;
}

static void *ko_digest_list_start(struct seq_file *m, loff_t *pos)
{
	struct ko_digest_item *item;
	loff_t value = *pos;

	rcu_read_lock();
	list_for_each_entry_rcu(item, &digest_list.list_head, list_node) {
		if (value-- == 0)
			return item;
	}
	return NULL;
}

static void ko_digest_list_stop(struct seq_file *m, void *v)
{
	rcu_read_unlock();
}

static void print_digest(struct seq_file *m, const u8 *digest)
{
	size_t i;

	for (i = 0; i < digest_list.digest_size; i++)
		seq_printf(m, "%02x", *(digest + i));
}

static int ko_digest_list_show(struct seq_file *m, void *v)
{
	struct ko_digest_item *item = v;

	if (!item)
		return -EINVAL;

	print_digest(m, item->digest);
	seq_puts(m, "\n");
	return 0;
}

static const struct seq_operations ko_digest_list_seqops = {
	.start = ko_digest_list_start,
	.next = ko_digest_list_next,
	.stop = ko_digest_list_stop,
	.show = ko_digest_list_show
};

static int ko_release_digest_list(struct inode *inode, struct file *file)
{
	if ((file->f_flags & O_ACCMODE) == O_RDONLY)
		return seq_release(inode, file);

	return 0;
}

static int ko_open_digest_list(struct inode *inode __always_unused, struct file *file)
{
	if ((file->f_flags & O_ACCMODE) == O_RDONLY)
		return seq_open(file, &ko_digest_list_seqops);
	return 0;
}

static const struct file_operations ko_digest_list_ops = {
	.open = ko_open_digest_list,
	.read = seq_read,
	.write = ko_write_digest_list,
	.release = ko_release_digest_list,
	.llseek = generic_file_llseek,
};

static void ko_digest_init(void)
{
	hash_init(digest_list.hash_head);
	INIT_LIST_HEAD_RCU(&digest_list.list_head);
	digest_list.digest_size = SHA256_DIGEST_SIZE;
	spin_lock_init(&digest_list.lock);
	digest_list.count = 0;
	digest_list.max = DEFAULT_KO_DIGEST_LIST_MAX;
}

static int __init ko_fs_init(void)
{
	ko_digest_init();
	ko_digest_list_data = securityfs_create_file("ko_digestlist", 0600,
						  NULL, NULL, &ko_digest_list_ops);
	if (IS_ERR(ko_digest_list_data)) {
		pr_err("securityfs_create_file ko_digestlist failed");
		return PTR_ERR(ko_digest_list_data);
	}
	return 0;
}
late_initcall(ko_fs_init);

static bool ko_digest_find_by_digest(const u8 *digest)
{
	unsigned int key;
	struct ko_digest_item *item;
	bool result = false;

	key = ko_hash_key(digest, digest_list.digest_size);
	rcu_read_lock();
	hash_for_each_possible_rcu(digest_list.hash_head, item, hash_node, key) {
		if (memcmp(item->digest, digest, digest_list.digest_size) == 0) {
			result = true;
			break;
		}
	}
	rcu_read_unlock();

	return result;
}

static bool calc_found_digest(const void *buf, size_t len)
{
	int rc;
	u8 digest[SHA256_DIGEST_SIZE] = { 0 };

	if (digest_list.count == 0)
		return false;

	rc = rtos_calc_sha256((const u8 *)buf, len, digest);
	if (rc != 0)
		return false;

	return ko_digest_find_by_digest(digest);
}

bool module_found_digest(const void *buf, size_t mod_len, size_t sig_len)
{
	return calc_found_digest(buf, mod_len + sig_len +
		sizeof(struct module_signature) + sizeof(MODULE_SIG_STRING) - 1) ||
		calc_found_digest(buf, mod_len);
}

